const net = require('net')
const io = require('socket.io-client')
const colors = require('colors/safe')
import crypto from 'crypto';

let data_c_id = 0

const encryptList = ['aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc']

interface ClientInfo {
  encrypt: string;
  key: string;
  iv: string;
  url: string;
}

class ClientServer {
  private encryptMethod: string;
  private key: string = "";
  private iv: string = "";

  constructor(argv: ClientInfo) {
    this.encryptMethod = argv.encrypt
    if (encryptList.includes(this.encryptMethod)) {
      this.key = argv.key
      this.iv = argv.iv
    }
    this.url = argv.url
    this.persistentConnectionUID = argv.unid
    if (!this.url) {
      console.log(colors.red('Error: Could not get url'))
    }
    // if (argv.binds) {
    //   this.binds = argv.binds
    // }
  }
  url
  /*
   *  example:
      structure:
    {
    wall: {
      local_port: 8080,
      remote_port: 10004
    },
    www: {
      local_port: 8090,
      remote_port: 9080
    },//本地服务名=>端口映射
  }
   */
  binds = {}
  isBinded = false
  isConnected = false
  isRunning = false
  mainsSocket: Object = {}
  localConnectingSockets = {}
  persistentConnectionUID
  reconnetInterval: number = -1
  logger = {
    info: console.log,
  }

  init() {
    this.localConnectingSockets = {}
    this.mainsSocket = io(this.url, )
    console.log(`connect to ${this.url}`)
    // socket.on()用于接收服务端发来的消息
    this.mainsSocket.on('connect', () => {
      clearInterval(this.reconnetInterval)
      this.reconnetInterval = -1
      this.isConnected = true
      console.log('client connect server')
    })

    this.mainsSocket.on('base config', (res: { clients: Array<any>}) => {
      const { clients } = res
      Object.keys(clients[this.persistentConnectionUID].binds).forEach(
        (key) => {
          this.binds[this.persistentConnectionUID + key] =
            clients[this.persistentConnectionUID].binds[key]
        }
      )
      this.mainsSocket.emit(
        'persistentConnectionUID',
        this.encrypt(this.persistentConnectionUID)
      )

      console.log(this.binds)
      for (let serverKey in this.binds) {
        console.log(serverKey)
        this.mainsSocket.on(serverKey, async (d) => {
          console.log(d)
          await this.handleDataFromFrontServer(serverKey, d)
        })
      }
    })

    this.mainsSocket.on('disconnect', (reason) => {
      console.log(`长连断开，原因: ${reason}`)
      console.log('处理disconnect事件')
      this.isConnected = false
      try {
        console.log('重连')
        this.reconnetInterval = setInterval(() => {
          if (!this.isConnected) {
            this.mainsSocket.close()
            this.clear()
            this.init()
          }
        }, 10 * 1000)
      } catch (err) {
        console.error(err)
      }
    })

    this.mainsSocket.on('ping', () => {
      console.log('[E] 心跳请求已发出 →')
    })
    //收到
    this.mainsSocket.on('pong', () => {
      console.log('[E] 心跳响应已收到 ←')
    })
  }
  // 关闭与源服务的连接
  encrypt(data) {
    if (this.key) {
      const cipher = crypto.createCipheriv(
        this.encryptMethod,
        this.key,
        this.iv
      )
      let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex')
      encrypted += cipher.final('hex')
      return encrypted
    }
    return data
  }
  encryptBuffer(buf) {
    if (this.key) {
      const cipher = crypto.createCipheriv(
        this.encryptMethod,
        this.key,
        this.iv
      )
      return Buffer.concat([cipher.update(buf), cipher.final()])
    }
    return buf
  }
  decryptBuffer(buf) {
    if (this.key) {
      const decipher = crypto.createDecipheriv(
        this.encryptMethod,
        this.key,
        this.iv
      )
      return Buffer.concat([decipher.update(buf), decipher.final()])
    }
    return buf
  }

  clear() {
    for (var key in this.localConnectingSockets) {
      try {
        this.localConnectingSockets[key].end()
        this.localConnectingSockets[key] = null
        delete this.localConnectingSockets[key]
      } catch (err) {
        console.error(err)
      }
    }
  }
  // 向指定ip和端口的服务源发送请求数据
  async createLocalConnectingSocket(serverKey, requestKey) {
    const that = this
    return new Promise((resolve, reject) => {
      this.localConnectingSockets[requestKey] = net.connect(
        {
          host: this.binds[serverKey].local_host,
          port: this.binds[serverKey].local_port,
        },
        () => {
          console.log('监听端口: ', this.binds[serverKey].local_port);
          resolve(true)
        }
      )

      this.localConnectingSockets[requestKey].on('data', (data) => {
        console.log(
          `send s data: ${Buffer.from(data).toString('utf-8', 0, 100)}`
        )
        try {
          that.mainsSocket.emit(serverKey, {
            cmd: 'data',
            serverKey,
            requestKey,
            data: this.encryptBuffer(data),
            data_c_id: data_c_id++,
          })
        } catch (err) {
          console.error(err)
        }
      })
      this.localConnectingSockets[requestKey].on('error', (err) => {
        console.error(err)
        that.handleWhenErrorOrClose(serverKey, requestKey)
        reject(err)
      })
      this.localConnectingSockets[requestKey].on('close', () => {
        that.handleWhenErrorOrClose(serverKey, requestKey)
        reject()
      })
      const tmp = setTimeout(() => {
        clearTimeout(tmp)
        reject()
      }, 10 * 1000)
    })
  }
  // 处理错误或异常关闭
  handleWhenErrorOrClose(serverKey, requestKey) {
    if (!this.localConnectingSockets[requestKey]) {
      return
    }
    this.localConnectingSockets[requestKey].end()
    this.localConnectingSockets[requestKey] = null
    delete this.localConnectingSockets[requestKey]

    this.mainsSocket.emit(serverKey, { cmd: 'close', requestKey })
  }

  async handleDataFromFrontServer(serverKey, d) {
    const { cmd, requestKey, data } = d
    if (cmd === 'data') {
      const decrypted = this.decryptBuffer(data)
      console.log(
        `receive s data: ${Buffer.from(decrypted).toString('utf-8', 0, 100)}`
      )

      try {
        let res = true
        if (!this.localConnectingSockets[requestKey]) {
          console.log('创建本地转发连接')
          res = await this.createLocalConnectingSocket(serverKey, requestKey)
        }
        console.log('向本地连接发送数据')
        if (res === true && this.localConnectingSockets[requestKey]) {
          this.localConnectingSockets[requestKey].write(decrypted)
        }
      } catch (err) {
        console.error('error from catch', err)
        this.localConnectingSockets[requestKey].end()
        this.localConnectingSockets[requestKey] = null
        delete this.localConnectingSockets[requestKey]
        this.handleWhenErrorOrClose(serverKey, requestKey)
      }
    } else if (cmd === 'close') {
      console.log(`close requestKey: ${requestKey}`)
      if (!this.localConnectingSockets[requestKey]) {
        return
      }
      this.localConnectingSockets[requestKey].end()
      this.localConnectingSockets[requestKey] = null
      delete this.localConnectingSockets[requestKey]
    }
  }

  /*
   * binds: ex:['wall', 'www']
   */
  removeBinds(binds) {
    this.mainsSocket.emit('removeBinds', this.encrypt(binds))
  }

  /*
   {
    wall: {
      local_port: 8080,
      remote_port: 10004
    }
   */
  addBinds(binds) {
    const remote_ports = []
    for (let key in binds) {
      // 如果用户没有设置服务源ip,则把ip设置为127.0.0.1
      if (!binds[key].remote_ip) {
        binds[key].remote_ip = '127.0.0.1'
      }

      remote_ports.push(binds[key].remote_port)
      if (this.binds[key]) {
        throw `Error: the bind of '${key}' existed`
      }
    }

    for (let key in this.binds) {
      if (remote_ports.includes(this.binds[key].remote_port)) {
        throw `Error: the remote port of '${this.binds[key].remote_port}' existed`
      }
    }

    console.log(`before, binds: ${JSON.stringify(this.binds)}`)
    this.binds = { ...this.binds, ...binds }
    console.log(`after, binds: ${JSON.stringify(this.binds)}`)
    this.mainsSocket.emit('addBinds', this.encrypt(binds))
    for (let serverKey in binds) {
      this.mainsSocket.on(serverKey, async (d) => {
        await this.handleDataFromFrontServer(serverKey, d)
      })
    }
  }

  start() {
    this.init()
  }
}

module.exports = { ClientServer }
